package com.smartandroid.sa.tag.parser;

import com.smartandroid.sa.tag.helper.Validate;
import com.smartandroid.sa.tag.nodes.Attribute;
import com.smartandroid.sa.tag.nodes.Attributes;

/**
 * Parse tokens for the Tokeniser.
 */
abstract class Token {
	TokenType type;

	private Token() {
	}

	String tokenType() {
		return this.getClass().getSimpleName();
	}

	static class Doctype extends Token {
		final StringBuilder name = new StringBuilder();
		final StringBuilder publicIdentifier = new StringBuilder();
		final StringBuilder systemIdentifier = new StringBuilder();
		boolean forceQuirks = false;

		Doctype() {
			type = TokenType.Doctype;
		}

		String getName() {
			return name.toString();
		}

		String getPublicIdentifier() {
			return publicIdentifier.toString();
		}

		public String getSystemIdentifier() {
			return systemIdentifier.toString();
		}

		public boolean isForceQuirks() {
			return forceQuirks;
		}
	}

	static abstract class Tag extends Token {
		protected String tagName;
		private String pendingAttributeName; // attribute names are generally
												// caught in one hop, not
												// accumulated
		private StringBuilder pendingAttributeValue; // but values are
														// accumulated, from
														// e.g. & in hrefs

		boolean selfClosing = false;
		Attributes attributes; // start tags get attributes on construction. End
								// tags get attributes on first new attribute
								// (but only for parser convenience, not used).

		void newAttribute() {
			if (attributes == null)
				attributes = new Attributes();

			if (pendingAttributeName != null) {
				Attribute attribute;
				if (pendingAttributeValue == null)
					attribute = new Attribute(pendingAttributeName, "");
				else
					attribute = new Attribute(pendingAttributeName,
							pendingAttributeValue.toString());
				attributes.put(attribute);
			}
			pendingAttributeName = null;
			if (pendingAttributeValue != null)
				pendingAttributeValue.delete(0, pendingAttributeValue.length());
		}

		void finaliseTag() {
			// finalises for emit
			if (pendingAttributeName != null) {
				// todo: check if attribute name exists; if so, drop and error
				newAttribute();
			}
		}

		String name() {
			Validate.isFalse(tagName.length() == 0);
			return tagName;
		}

		Tag name(String name) {
			tagName = name;
			return this;
		}

		boolean isSelfClosing() {
			return selfClosing;
		}

		@SuppressWarnings({ "TypeMayBeWeakened" })
		Attributes getAttributes() {
			return attributes;
		}

		// these appenders are rarely hit in not null state-- caused by null
		// chars.
		void appendTagName(String append) {
			tagName = tagName == null ? append : tagName.concat(append);
		}

		void appendTagName(char append) {
			appendTagName(String.valueOf(append));
		}

		void appendAttributeName(String append) {
			pendingAttributeName = pendingAttributeName == null ? append
					: pendingAttributeName.concat(append);
		}

		void appendAttributeName(char append) {
			appendAttributeName(String.valueOf(append));
		}

		void appendAttributeValue(String append) {
			ensureAttributeValue();
			pendingAttributeValue.append(append);
		}

		void appendAttributeValue(char append) {
			ensureAttributeValue();
			pendingAttributeValue.append(append);
		}

		void appendAttributeValue(char[] append) {
			ensureAttributeValue();
			pendingAttributeValue.append(append);
		}

		private final void ensureAttributeValue() {
			if (pendingAttributeValue == null)
				pendingAttributeValue = new StringBuilder();
		}
	}

	static class StartTag extends Tag {
		StartTag() {
			super();
			attributes = new Attributes();
			type = TokenType.StartTag;
		}

		StartTag(String name) {
			this();
			this.tagName = name;
		}

		StartTag(String name, Attributes attributes) {
			this();
			this.tagName = name;
			this.attributes = attributes;
		}

		@Override
		public String toString() {
			if (attributes != null && attributes.size() > 0)
				return "<" + name() + " " + attributes.toString() + ">";
			else
				return "<" + name() + ">";
		}
	}

	static class EndTag extends Tag {
		EndTag() {
			super();
			type = TokenType.EndTag;
		}

		EndTag(String name) {
			this();
			this.tagName = name;
		}

		@Override
		public String toString() {
			return "</" + name() + ">";
		}
	}

	static class Comment extends Token {
		final StringBuilder data = new StringBuilder();
		boolean bogus = false;

		Comment() {
			type = TokenType.Comment;
		}

		String getData() {
			return data.toString();
		}

		@Override
		public String toString() {
			return "<!--" + getData() + "-->";
		}
	}

	static class Character extends Token {
		private final String data;

		Character(String data) {
			type = TokenType.Character;
			this.data = data;
		}

		String getData() {
			return data;
		}

		@Override
		public String toString() {
			return getData();
		}
	}

	static class EOF extends Token {
		EOF() {
			type = Token.TokenType.EOF;
		}
	}

	boolean isDoctype() {
		return type == TokenType.Doctype;
	}

	Doctype asDoctype() {
		return (Doctype) this;
	}

	boolean isStartTag() {
		return type == TokenType.StartTag;
	}

	StartTag asStartTag() {
		return (StartTag) this;
	}

	boolean isEndTag() {
		return type == TokenType.EndTag;
	}

	EndTag asEndTag() {
		return (EndTag) this;
	}

	boolean isComment() {
		return type == TokenType.Comment;
	}

	Comment asComment() {
		return (Comment) this;
	}

	boolean isCharacter() {
		return type == TokenType.Character;
	}

	Character asCharacter() {
		return (Character) this;
	}

	boolean isEOF() {
		return type == TokenType.EOF;
	}

	enum TokenType {
		Doctype, StartTag, EndTag, Comment, Character, EOF
	}
}
